Adding New Contacts
In this step, we'll implement a modal that allows users to add new contacts by alias within the app. This process includes initializing the agent, connecting it to WebSocket (WS) sessions, and adding contacts to the app using the One37ID SDK.
Objective
- Set up WebSocket sessions for all existing connections when the app starts.
- Add new contacts through a modal by their alias.
1. Initializing the Agent in App.js
or App.tsx
To ensure that WebSocket sessions are established for existing connections, you need to initialize the agent and start the WebSocket connections when the app starts. Add the following code to App.js
or App.tsx
:
import { initializeAgent } from "./src/one37Agent";
import crypto from "@sphereon/isomorphic-webcrypto";
useEffect(() => {
const startAllWSSessions = async () => {
const agent = await initializeAgent();
if (!agent) {
throw new Error("Agent not initialized");
}
const connections = await agent.contactManager.getList({
currentPage: 1,
rowsPerPage: 1000,
});
if (connections.result) {
for (let connection of connections.result) {
await agent.contactManager.connectWS({ id: connection.id });
}
}
};
startAllWSSessions();
}, []);
useEffect(() => {
crypto.ensureSecure(); // Ensure secure crypto environment
}, []);
This block:
- Initializes the agent when the app starts.
- Retrieves existing contacts and establishes WebSocket sessions for each.
2. Adding Crypto to package.json
To ensure compatibility with the One37ID SDK and React Native's environment, ensure the following configuration to package.json
if they dosen't exists:
"browser": {
"crypto": "@sphereon/isomorphic-webcrypto"
}
And in react-native
:
"react-native": {
"crypto": "@sphereon/isomorphic-webcrypto"
}
This ensures that crypto
functions correctly in the app.
Run yarn install
afterwards.
3. Creating the Add Contact Modal
Now, we'll create a modal that enables users to add new contacts by their alias.
import React, { useState } from "react";
import {
View,
Text,
StyleSheet,
TouchableOpacity,
TextInput,
ActivityIndicator,
} from "react-native";
import Modal from "react-native-modal";
import { initializeAgent } from "../../one37Agent";
import { Contact, OperationDataResult } from "@one37id/mobile-js-sdk";
type AddContactModalProps = {
modalVisible: boolean,
setModalVisible: (visible: boolean) => void,
onContactAdded: () => void,
};
export const CustomAddContactModal = ({
modalVisible,
setModalVisible,
onContactAdded,
}: AddContactModalProps) => {
const [alias, setAlias] = useState("");
const [isLoading, setIsLoading] = useState(false);
const addByAlias = async () => {
if (alias.trim() === "") {
alert("Alias cannot be empty");
return;
}
setIsLoading(true);
try {
const res = await createContact(alias);
onContactAdded();
console.log("Contact added successfully:", res);
} catch (error) {
console.error("Error adding contact:", error);
} finally {
setIsLoading(false);
setModalVisible(false);
}
};
const createContact = async (alias: string) => {
const agent = await initializeAgent();
if (!agent) {
throw new Error("Agent not initialized");
}
return agent.contactManager
.addByAlias({ realm: alias })
.then((contact: OperationDataResult<Contact>) => {
if (!contact.isSuccessful) {
console.error(
`Failed to create contact for alias: ${alias}, error: ${contact.error}`
);
return Promise.reject(new Error(`${contact.error}`));
}
return contact.result;
})
.catch((error) => {
console.error(`Error while adding contact: ${error.message}`);
return Promise.reject(
new Error(`Unable to create contact. Error: ${error.message}`)
);
});
};
return (
<>
<Modal
isVisible={modalVisible}
onBackdropPress={() => setModalVisible(false)}
animationIn="slideInUp"
animationOut="slideOutDown"
backdropColor="rgba(0,0,0,0.5)"
backdropOpacity={0.7}
style={styles.modalContainer}
>
<View style={styles.modalContent}>
<Text style={styles.modalTitle}>Add New Contact</Text>
<TextInput
style={styles.input}
placeholder="Enter Alias"
value={alias}
onChangeText={setAlias}
/>
<TouchableOpacity style={styles.addButton} onPress={addByAlias}>
<Text style={styles.addButtonText}>Add Contact</Text>
</TouchableOpacity>
</View>
</Modal>
{isLoading && (
<ActivityIndicator
style={styles.loading}
size="large"
color="#00ff00"
/>
)}
</>
);
};
const styles = StyleSheet.create({
modalContainer: {
justifyContent: "center",
margin: 0,
},
modalContent: {
backgroundColor: "white",
padding: 20,
borderRadius: 10,
alignItems: "center",
justifyContent: "center",
},
modalTitle: {
fontSize: 18,
fontWeight: "bold",
marginBottom: 15,
},
input: {
width: "100%",
height: 40,
borderColor: "#ccc",
borderWidth: 1,
borderRadius: 5,
paddingHorizontal: 10,
marginBottom: 20,
},
addButton: {
backgroundColor: "#008080",
paddingVertical: 10,
paddingHorizontal: 20,
borderRadius: 5,
alignItems: "center",
},
addButtonText: {
color: "#fff",
fontWeight: "bold",
},
loading: {
position: "absolute",
top: "50%",
left: "50%",
zIndex: 1,
},
});
When initializing the agent, we need to include a callbackHandlers
object. This object will hold the necessary callback functions, such as the one used for handling contact approvals.
Here's how we set the addContactApprovalCallback
:
const callbackHandlers = {
addContactApprovalCallback: handleAddContactApproval,
};
Define the handleAddContactApproval
Function
The handleAddContactApproval
function is a key part of this flow. It gets triggered when a new contact is added by alias. This function navigates to a screen where the user can either approve or decline the contact request. Here's a simplified version of the function that handles contact approvals.
async function handleAddContactApproval(
contactArgs: NewContactInfo
): Promise<boolean> {
console.log("---handleAddContactApproval:", contactArgs);
return new Promise((resolve, reject) => {
try {
//Navigate to the add contact screen
RootNavigation.navigate("ContactAddScreen", {
contactData: contactArgs,
onDecline: () => {
console.log("---Contact declined");
resolve(false); // Return `false` to indicate the contact was not approved
},
onCreate: () => {
console.log("---Contact accepted");
resolve(true); // Return `true` to indicate the contact was approved
},
});
} catch (error) {
console.log("---Error during handleAddContactApproval:", error);
reject(error); // Handle any errors that may occur during navigation
}
});
}
Key Points
- Navigation to Approval Screen: The function uses
RootNavigation.navigate
to take the user to theContactAddScreen
where they can approve or decline the contact. - Callbacks for Approval/Decline: When the user accepts or rejects the contact, the respective callback is triggered (
onCreate
for approval andonDecline
for rejection).
Implement the ContactAddScreen
for Approval
The ContactAddScreen
is the UI where users can view details about the contact and choose to either approve or decline the contact request. Here’s a simple implementation:
import React, { FC } from "react";
import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
import { NativeStackScreenProps } from "@react-navigation/native-stack";
import FastImage from "react-native-fast-image";
// Define your stack params to type-check navigation params
type StackParamList = {
ContactAddScreen: {
contactData: { title: string, contactType: string },
onCreate: () => void,
onDecline: () => void,
},
};
type Props = NativeStackScreenProps<StackParamList, "ContactAddScreen">;
const ContactAddScreen: FC<Props> = ({ route, navigation }) => {
const { contactData, onCreate, onDecline } = route.params;
console.log("params", route.params);
console.log("contactData:", contactData);
const handleCreate = async () => {
if (onCreate) {
await onCreate();
navigation.goBack();
}
};
const handleDecline = async () => {
if (onDecline) {
await onDecline();
navigation.goBack();
}
};
return (
<View style={styles.container}>
{/* Header Section with Title and Image */}
<View style={styles.headerContainer}>
<Text style={styles.title}>Connection Request</Text>
{contactData?.logoUrl ? (
<FastImage
source={{ uri: contactData.logoUrl }}
style={styles.logoImage}
resizeMode={FastImage.resizeMode.contain} // Use FastImage's resizeMode
/>
) : null}
</View>
{/* Details Section */}
<View style={styles.details}>
<Text style={styles.contactTitle}>{contactData?.title}</Text>
<Text style={styles.contactType}>Type: {contactData?.contactType}</Text>
</View>
{/* Action Buttons */}
<View style={styles.buttonsContainer}>
<TouchableOpacity style={styles.button} onPress={handleDecline}>
<Text style={styles.buttonText}>Reject</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={handleCreate}>
<Text style={styles.buttonText}>Accept</Text>
</TouchableOpacity>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
headerContainer: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
title: {
fontSize: 22,
fontWeight: "bold",
},
logoImage: {
width: 60,
height: 60,
marginRight: 20,
},
details: {
marginTop: 20,
},
contactTitle: {
fontSize: 18,
fontWeight: "600",
},
contactType: {
fontSize: 16,
color: "gray",
marginTop: 5,
},
buttonsContainer: {
flexDirection: "row",
justifyContent: "space-between",
marginTop: 30,
},
button: {
backgroundColor: "#007BFF",
paddingVertical: 10,
paddingHorizontal: 20,
borderRadius: 5,
},
buttonText: {
color: "#fff",
fontSize: 16,
},
});
export default ContactAddScreen;
Explanation of the ContactAddScreen
- Header Section: Displays the connection request title and logo (if available).
- Details Section: Shows the contact's title and type.
- Action Buttons: The user can either "Accept" or "Reject" the contact request by clicking the respective button.
- Accept: Triggers the
onCreate
callback and resolves the promise. - Reject: Triggers the
onDecline
callback and resolves the promise with a rejection.
- Accept: Triggers the